﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Castle.Core;
using Castle.Core.Configuration;
using Castle.MicroKernel;
using Castle.MicroKernel.ComponentActivator;
using Castle.MicroKernel.Context;
using Castle.MicroKernel.SubSystems.Conversion;

namespace WindsorInitConfig {
    public class InitComponentActivator : DefaultComponentActivator {
        public virtual MethodInfo GetMatchingMethod(IConfiguration methodConfig, Type t) {
            var configParameterNames = methodConfig.Children.Select(c => c.Name).ToArray();
            var method = t.GetMethods()
                .Where(m => m.Name == methodConfig.Name)
                .FirstOrDefault(m => m.GetParameters().Select(p => p.Name).SequenceEqual(configParameterNames));
            if (method == null)
                throw new ComponentActivatorException(string.Format("No method '{0}' with parameters {1} found",
                                                                    methodConfig.Name,
                                                                    string.Join(", ", configParameterNames)));
            return method;
        }

        public InitComponentActivator(ComponentModel model, IKernel kernel, ComponentInstanceDelegate onCreation, ComponentInstanceDelegate onDestruction) : base(model, kernel, onCreation, onDestruction) {}

        public string MethodCallToString(MethodInfo method, object[] parameters) {
            return string.Format("{0}({1})", method.Name,
                                 string.Join(", ", Zip(method.GetParameters(), parameters)
                                                       .Select(p => string.Format("{0} {1}", p.Key.ParameterType, p.Value))
                                                       .ToArray()));
        }

        public override object Create(CreationContext context) {
            var component = base.Create(context);
            var initConfig = Model.Configuration.Children["init"];
            if (initConfig == null)
                return component;
            foreach (var methodConfig in initConfig.Children) {
                var method = GetMatchingMethod(methodConfig, Model.Implementation);
                var parameters = GetParameters(methodConfig, method);
                try {
                    method.Invoke(component, parameters);
                } catch (Exception e) {
                    throw new ComponentActivatorException(string.Format("Exception initializing component id '{0}', service '{1}', implementation '{2}' with method '{3}'",
                                                                        Model.Name,
                                                                        Model.Service,
                                                                        Model.Implementation,
                                                                        MethodCallToString(method, parameters)), e);
                }
            }
            return component;
        }

        /// <summary>
        /// From http://community.bartdesmet.net/blogs/bart/archive/2008/11/03/c-4-0-feature-focus-part-3-intermezzo-linq-s-new-zip-operator.aspx
        /// </summary>
        public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> func) {
            if (first == null)
                throw new ArgumentNullException("first");
            if (second == null)
                throw new ArgumentNullException("second");
            if (func == null)
                throw new ArgumentNullException("func");
            using (var ie1 = first.GetEnumerator())
            using (var ie2 = second.GetEnumerator())
                while (ie1.MoveNext() && ie2.MoveNext())
                    yield return func(ie1.Current, ie2.Current);
        }

        public static IEnumerable<KeyValuePair<T, R>> Zip<T, R>(IEnumerable<T> first, IEnumerable<R> second) {
            return Zip(first, second, (f, s) => new KeyValuePair<T, R>(f, s));
        }

        public virtual object[] GetParameters(IConfiguration config, MethodInfo method) {
            var converter = (IConversionManager) Kernel.GetSubSystem(SubSystemConstants.ConversionManagerKey);
            try {
                return Zip(config.Children, method.GetParameters())
                    .Select(c => converter.PerformConversion(c.Key, c.Value.ParameterType)).ToArray();
            } catch (Exception e) {
                throw new ComponentActivatorException("Couldn't convert parameters", e);
            }
        }
    }
}